package org.jing.ext.jdbc.mybatis;

import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.TransactionIsolationLevel;
import org.jing.core.lang.JingException;
import org.jing.core.logger.JingLogger;
import org.jing.core.util.StringUtil;
import org.jing.ext.jdbc.JDBCBuilder;
import org.jing.ext.jdbc.JingJdbcException;

import java.io.Closeable;
import java.io.IOException;
import java.sql.Connection;
import java.util.Map;
import java.util.Stack;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Description: MyBatis操作类. <br>
 *
 * @author bks <br>
 * @since 2021-08-17 <br>
 */
@SuppressWarnings("unchecked")
public class MyBatis implements Closeable {
    private static final JingLogger LOGGER = JingLogger.getLogger(MyBatis.class);

    private SqlSessionFactory sessionFactory;

    private Stack<SqlSession> sessionStack;

    private boolean closed = false;

    private ConcurrentHashMap<Class, Object> domainMap = new ConcurrentHashMap<>();

    private volatile static JDBCBuilder<SqlSessionFactory> mainBuilder = null;

    private static final ConcurrentHashMap<String, JDBCBuilder<SqlSessionFactory>> SUB_BUILDER = new ConcurrentHashMap<>();

    private volatile static SqlSessionFactory mainSessionFactory = null;

    private static final ConcurrentHashMap<String, SqlSessionFactory> SUB_SESSION_FACTORY = new ConcurrentHashMap<>();

    private static final ThreadLocal<MyBatis> MAIN_JDBC = new ThreadLocal<>();

    private static final ThreadLocal<ConcurrentHashMap<String, MyBatis>> SUB_JDBC = new ThreadLocal<>();

    public static MyBatis get() throws JingJdbcException {
        MyBatis myBatis = MAIN_JDBC.get();
        if (null == myBatis) {
            synchronized (MyBatis.class) {
                LOGGER.debug("create main MyBatis for current thread: {}", Thread.currentThread().getName());
                myBatis = new MyBatis();
                LOGGER.debug("[{}] main MyBatis created", myBatis.hashCode());
                MAIN_JDBC.set(myBatis);
            }
        }
        if (null == mainSessionFactory) {
            if (null == mainBuilder) {
                throw new JingJdbcException("null main builder");
            }
            synchronized (MyBatis.class) {
                try {
                    mainSessionFactory = mainBuilder.build();
                }
                catch (JingException e) {
                    throw e;
                }
                catch (Throwable t) {
                    throw new JingJdbcException(t, "failed to build session factory");
                }
            }
        }
        if (null == myBatis.sessionFactory) {
            synchronized (MyBatis.class) {
                myBatis.sessionFactory = mainSessionFactory;
            }
        }
        return myBatis;
    }

    public static MyBatis getSub(String code) throws JingJdbcException {
        if (StringUtil.isEmpty(code)) {
            throw new JingJdbcException("code for sub MyBatis is required");
        }
        JDBCBuilder<SqlSessionFactory> subBuilder;
        synchronized (MyBatis.class) {
            subBuilder = SUB_BUILDER.get(code);
            if (null == subBuilder) {
                throw new JingJdbcException("sub Builder is null for code [{}]", code);
            }
        }
        ConcurrentHashMap<String, MyBatis> subJdbcMap = SUB_JDBC.get();
        if (null == subJdbcMap) {
            subJdbcMap = new ConcurrentHashMap<>();
            SUB_JDBC.set(subJdbcMap);
        }
        MyBatis subMyBatis = subJdbcMap.get(code);
        if (null == subMyBatis) {
            synchronized (MyBatis.class) {
                LOGGER.debug("create sub MyBatis [{}] for current thread: {}", code, Thread.currentThread().getName());
                subMyBatis = new MyBatis();
                LOGGER.debug("[{}] sub MyBatis created", subMyBatis.hashCode());
                subJdbcMap.put(code, subMyBatis);
            }
        }
        SqlSessionFactory subSessionFactory = SUB_SESSION_FACTORY.get(code);
        if (null == subSessionFactory) {
            synchronized (MyBatis.class) {
                try {
                    subSessionFactory = subBuilder.build();
                    SUB_SESSION_FACTORY.put(code, subSessionFactory);
                }
                catch (JingException e) {
                    throw e;
                }
                catch (Throwable t) {
                    throw new JingJdbcException(t, "failed to build session factory");
                }
            }
        }
        if (null == subMyBatis.sessionFactory) {
            synchronized (MyBatis.class) {
                subMyBatis.sessionFactory = subSessionFactory;
            }
        }
        return subMyBatis;
    }

    private MyBatis() {
        this.sessionStack = new Stack<>();
    }

    /**
     * Description: 析构函数. <br>
     *     会执行close()方法. <br>
     *
     * @throws Throwable <br>
     */
    @Override
    protected void finalize() throws Throwable {
        close();
        super.finalize();
    }

    private void setSessionFactory(SqlSessionFactory sessionFactory) {

    }

    /**
     * Description: 设置Builder. <br>
     *
     * @param builder <br>
     */
    public static synchronized void setBuilder(JDBCBuilder<SqlSessionFactory> builder) {
        synchronized (MyBatis.class) {
            MyBatis.mainBuilder = builder;
        }
    }

    public static synchronized void setSubBuilder(String code, JDBCBuilder<SqlSessionFactory> builder) throws JingJdbcException {
        if (StringUtil.isEmpty(code)) {
            throw new JingJdbcException("code for sub MyBatis is required");
        }
        synchronized (MyBatis.class) {
            SUB_BUILDER.put(code, builder);
        }
    }

    /**
     * Description: 使用当前的Builder重新创建SessionFactory. <br>
     *
     * @throws JingJdbcException <br>
     */
    public static synchronized void reBuild() throws JingJdbcException {
        if (null == MyBatis.mainBuilder) {
            throw new JingJdbcException("null mainBuilder");
        }
        synchronized (MyBatis.class) {
            try {
                mainSessionFactory = MyBatis.mainBuilder.build();
            }
            catch (JingException e) {
                throw e;
            }
            catch (Throwable t) {
                throw new JingJdbcException(t, "failed to re-build session factory");
            }
        }
        LOGGER.debug("re-build success");
    }

    /**
     * Description: 使用指定的Builder重新创建SessionFactory. <br>
     *
     * @param builder <br>
     * @throws JingJdbcException <br>
     */
    public static synchronized void reBuild(JDBCBuilder<SqlSessionFactory> builder) throws JingJdbcException {
        if (null == builder) {
            throw new JingJdbcException("null mainBuilder");
        }
        synchronized (MyBatis.class) {
            MyBatis.mainBuilder = builder;
            try {
                mainSessionFactory = MyBatis.mainBuilder.build();
            }
            catch (JingException e) {
                throw e;
            }
            catch (Throwable t) {
                throw new JingJdbcException(t, "failed to re-build session factory");
            }
        }
        LOGGER.debug("re-build success");
    }

    private void checkEnabled() throws JingJdbcException {
        if (closed) {
            throw new JingJdbcException("current MyBatis has been closed");
        }
        synchronized (MyBatis.class) {
            if (null == sessionFactory) {
                throw new JingJdbcException("null session factory");
            }
        }
    }

    public SqlSession newSession(ExecutorType executorType, boolean autoCommit) throws JingJdbcException {
        checkEnabled();
        SqlSession session = sessionFactory.openSession(executorType, autoCommit);
        sessionStack.push(session);
        return session;
    }

    public SqlSession newSession(ExecutorType executorType, TransactionIsolationLevel level) throws JingJdbcException {
        checkEnabled();
        SqlSession session = sessionFactory.openSession(executorType, level);
        sessionStack.push(session);
        return session;
    }

    public SqlSession newSession(ExecutorType executorType, Connection connection) throws JingJdbcException {
        checkEnabled();
        SqlSession session = sessionFactory.openSession(executorType, connection);
        sessionStack.push(session);
        return session;
    }

    /**
     * Description: 创建一个新的session. <br>
     * @param autoCommit 自动提交 <br>
     * @return 新的session <br>
     * @throws JingJdbcException <br>
     */
    public SqlSession newSession(boolean autoCommit) throws JingJdbcException {
        checkEnabled();
        SqlSession session = sessionFactory.openSession(autoCommit);
        sessionStack.push(session);
        return session;
    }

    public SqlSession newSession(Connection connection) throws JingJdbcException {
        checkEnabled();
        SqlSession session = sessionFactory.openSession(connection);
        sessionStack.push(session);
        return session;
    }

    public SqlSession newSession(ExecutorType executorType) throws JingJdbcException {
        checkEnabled();
        SqlSession session = sessionFactory.openSession(executorType);
        sessionStack.push(session);
        return session;
    }

    public SqlSession newSession(TransactionIsolationLevel level) throws JingJdbcException {
        checkEnabled();
        SqlSession session = sessionFactory.openSession(level);
        sessionStack.push(session);
        return session;
    }

    /**
     * Description: 创建一个新的, 自动提交为false的session. <br>
     *
     * @return 新的session <br>
     * @throws JingJdbcException <br>
     */
    public SqlSession newSession() throws JingJdbcException {
        checkEnabled();
        SqlSession session = sessionFactory.openSession();
        sessionStack.push(session);
        return session;
    }

    /**
     * Description: 获取最外层session并从栈里移除它. <br>
     *
     * @return 最外层session <br>
     * @throws JingJdbcException <br>
     */
    public SqlSession removeSession() throws JingJdbcException {
        checkEnabled();
        if (sessionStack.isEmpty()) {
            throw new JingJdbcException("session stack is empty");
        }
        return sessionStack.pop();
    }

    public SqlSession session(ExecutorType executorType, boolean autoCommit) throws JingJdbcException {
        checkEnabled();
        if (sessionStack.isEmpty()) {
            return newSession(executorType, autoCommit);
        }
        return sessionStack.peek();
    }

    public SqlSession session(ExecutorType executorType, TransactionIsolationLevel level) throws JingJdbcException {
        checkEnabled();
        if (sessionStack.isEmpty()) {
            return newSession(executorType, level);
        }
        return sessionStack.peek();
    }

    public SqlSession session(ExecutorType executorType, Connection connection) throws JingJdbcException {
        checkEnabled();
        if (sessionStack.isEmpty()) {
            return newSession(executorType, connection);
        }
        return sessionStack.peek();
    }

    public SqlSession session(Connection connection) throws JingJdbcException {
        checkEnabled();
        if (sessionStack.isEmpty()) {
            return newSession(connection);
        }
        return sessionStack.peek();
    }

    public SqlSession session(ExecutorType executorType) throws JingJdbcException {
        checkEnabled();
        if (sessionStack.isEmpty()) {
            return newSession(executorType);
        }
        return sessionStack.peek();
    }

    public SqlSession session(TransactionIsolationLevel level) throws JingJdbcException {
        checkEnabled();
        if (sessionStack.isEmpty()) {
            return newSession(level);
        }
        return sessionStack.peek();
    }

    /**
     * Description: 获取最外层的session. <br>
     *     如果当前栈为空, 则创建一个新的session. <br>
     *
     * @param autoCommit 自动提交 <br>
     * @return 最外层的session <br>
     * @throws JingJdbcException <br>
     */
    public SqlSession session(boolean autoCommit) throws JingJdbcException {
        checkEnabled();
        if (sessionStack.isEmpty()) {
            return newSession(autoCommit);
        }
        return sessionStack.peek();
    }

    /**
     * Description: 获取最外层的session. <br>
     *     如果当前栈为空, 则创建一个新的session. <br>
     *     如果需要创建新的session, 则指定自动提交为false. <br>
     *
     * @return 最外层的session <br>
     * @throws JingJdbcException <br>
     */
    public SqlSession session() throws JingJdbcException {
        checkEnabled();
        if (sessionStack.isEmpty()) {
            return newSession();
        }
        return sessionStack.peek();
    }

    /**
     * Description: 提交最外层的session. <br>
     *
     * @param force 强制执行 <br>
     * @throws JingJdbcException <br>
     */
    public void commit(boolean force) throws JingJdbcException {
        checkEnabled();
        if (sessionStack.isEmpty()) {
            throw new JingJdbcException("session stack is empty");
        }
        sessionStack.peek().commit(force);
    }

    /**
     * Description: 提交最外层的session. <br>
     *     强制执行标志为false. <br>
     *
     * @throws JingJdbcException <br>
     */
    public void commit() throws JingJdbcException {
        commit(false);
    }

    /**
     * Description: 提交所有的session. <br>
     *     从最外层往里进行提交. <br>
     *
     * @param force 强制执行 <br>
     * @throws JingJdbcException <br>
     */
    public void commitAll(boolean force) throws JingJdbcException {
        checkEnabled();
        int size = sessionStack.size();
        if (size > 0) {
            LOGGER.debug("commit all session, force: [{}]", force);
        }
        SqlSession session;
        for (int i$ = size - 1; i$ >= 0; i$--) {
            session = sessionStack.get(i$);
            LOGGER.debug("commit session: [{}], force: [{}]", session.getConnection().hashCode(), force);
            session.commit(force);
        }
    }

    /**
     * Description: 提交所有的session. <br>
     *     强制执行为false. <br>
     *     从最外层往里进行提交. <br>
     *
     * @throws JingJdbcException <br>
     */
    public void commitAll() throws JingJdbcException {
        commitAll(false);
    }

    /**
     * Description: 回滚最外层的session. <br>
     *
     * @param force 强制执行 <br>
     * @throws JingJdbcException <br>
     */
    public void rollback(boolean force) throws JingJdbcException {
        checkEnabled();
        if (sessionStack.isEmpty()) {
            throw new JingJdbcException("session stack is empty");
        }
        sessionStack.peek().rollback(force);
    }

    /**
     * Description: 回滚最外层的session. <br>
     *     强制执行标志为false. <br>
     *
     * @throws JingJdbcException <br>
     */
    public void rollback() throws JingJdbcException {
        rollback(false);
    }

    /**
     * Description: 回滚所有的session. <br>
     *     从最外层往里进行回滚. <br>
     *
     * @param force 强制执行 <br>
     * @throws JingJdbcException <br>
     */
    public void rollbackAll(boolean force) throws JingJdbcException {
        checkEnabled();
        int size = sessionStack.size();
        if (size > 0) {
            LOGGER.debug("rollback all session, force: [{}]", force);
        }
        SqlSession session;
        for (int i$ = size - 1; i$ >= 0; i$--) {
            session = sessionStack.get(i$);
            LOGGER.debug("rollback session: [{}], force: [{}]", session.getConnection().hashCode(), force);
            session.rollback(force);
        }
    }

    /**
     * Description: 回滚所有的session. <br>
     *     强制执行为false. <br>
     *     从最外层往里进行回滚. <br>
     *
     * @throws JingJdbcException <br>
     */
    public void rollbackAll() throws JingJdbcException {
        rollbackAll(false);
    }

    /**
     * Description: 释放最外层的session. <br>
     *     释放会导致对应session提交、关闭并出栈. <br>
     *
     * @throws JingJdbcException <br>
     */
    public void release() throws JingJdbcException {
        checkEnabled();
        if (sessionStack.isEmpty()) {
            throw new JingJdbcException("session stack is empty");
        }
        SqlSession session = sessionStack.pop();
        LOGGER.debug("release the top session: [{}]", session.getConnection().hashCode());
        session.commit(true);
        session.close();
    }

    /**
     * Description: 释放指定session以及其所有子session. <br>
     *     释放会导致对应session提交、关闭并出栈. <br>
     *
     * @param session 指定session <br>
     * @throws JingJdbcException <br>
     */
    public void release(SqlSession session) throws JingJdbcException {
        if (null == session) {
            LOGGER.error("session to be released is null");
            return;
        }
        checkEnabled();
        LOGGER.debug("release session: [{}]", session.getConnection().hashCode());
        if (!sessionStack.contains(session)) {
            throw new JingJdbcException("session doesn't exists in stack");
        }
        SqlSession tempS;
        do {
            tempS = sessionStack.pop();
            if (tempS.getConnection().hashCode() != session.getConnection().hashCode()) {
                LOGGER.debug("release child session: [{}]", tempS.getConnection().hashCode());
            }
            tempS.commit(true);
            tempS.close();
        }
        while (!session.equals(tempS));

    }

    /**
     * Description: 释放所有的session. <br>
     *     释放会导致对应session提交、关闭并出栈. <br>
     *
     * @throws JingJdbcException <br>
     */
    public void releaseAll() throws JingJdbcException {
        checkEnabled();
        LOGGER.debug("release all session...");
        SqlSession session;
        while (!sessionStack.isEmpty()) {
            session = sessionStack.pop();
            LOGGER.debug("release session: [{}]", session.getConnection().hashCode());
            session.commit(true);
            session.close();
        }
    }

    /**
     * Description: 等同releaseAll. <br>
     *
     * @throws IOException <br>
     */
    @Override
    public void close() throws IOException {
        try {
            releaseAll();
            if (this == MAIN_JDBC.get()) {
                LOGGER.debug("close main MyBatis [{}]", hashCode());
                MAIN_JDBC.remove();
                ConcurrentHashMap<String, MyBatis> subMyBatisMap = SUB_JDBC.get();
                if (null != subMyBatisMap) {
                    for (Map.Entry<String, MyBatis> sub : subMyBatisMap.entrySet()) {
                        sub.getValue().close();
                    }
                }
            }
            else {
                synchronized (SUB_JDBC) {
                    ConcurrentHashMap<String, MyBatis> subMyBatisMap = SUB_JDBC.get();
                    if (null != subMyBatisMap) {
                        String code = null;
                        for (Map.Entry<String, MyBatis> sub : subMyBatisMap.entrySet()) {
                            if (this == sub.getValue()) {
                                LOGGER.debug("close sub MyBatis [{}][{}]", sub.getKey(), hashCode());
                                code = sub.getKey();
                                break;
                            }
                        }
                        if (null != code) {
                            subMyBatisMap.remove(code);
                        }
                        if (subMyBatisMap.isEmpty()) {
                            SUB_JDBC.remove();
                        }
                    }
                }
            }

            closed = true;
        }
        catch (JingException e) {
            throw e;
        }
        catch (Throwable t) {
            throw new IOException(t);
        }
    }

    public static void closeSub(String code) throws IOException {
        synchronized (MyBatis.class) {
            ConcurrentHashMap<String, MyBatis> subMyBatisMap = SUB_JDBC.get();
            if (null == subMyBatisMap || !subMyBatisMap.containsKey(code)) {
                return;
            }
            for (Map.Entry<String, MyBatis> sub : subMyBatisMap.entrySet()) {
                if (sub.getKey().equals(code)) {
                    sub.getValue().close();
                    break;
                }
            }
            subMyBatisMap.remove(code);
            if (subMyBatisMap.isEmpty()) {
                SUB_JDBC.remove();
            }
        }
    }

    @Deprecated
    public <T> T createMyBatisDomain(Class<T> clazz) throws JingJdbcException {
        if (!BaseMyBatisDomain.class.isAssignableFrom(clazz)) {
            throw new JingJdbcException("class must extend from BaseMyBatisDomain");
        }
        try {
            T instance = clazz.newInstance();
            ((BaseMyBatisDomain) instance).setMyBatis(this);
            return instance;
        }
        catch (JingException e) {
            throw e;
        }
        catch (Throwable t) {
            throw new JingJdbcException(t, "failed to create domain");
        }
    }

    /**
     * Description: 获取一个继承了BaseMyBatisDomain.class的domain. <br>
     * 如果没有则会创建. <br>
     *
     * @param clazz 继承了BaseMyBatisDomain的class <br>
     * @param <T> <br>
     * @return domain <br>
     * @throws JingJdbcException <br>
     */
    public <T> T getMayBatisDomain(Class<T> clazz) throws JingJdbcException {
        if (!BaseMyBatisDomain.class.isAssignableFrom(clazz)) {
            throw new JingJdbcException("class must extend from BaseMyBatisDomain");
        }
        if (domainMap.containsKey(clazz)) {
            return (T) domainMap.get(clazz);
        }
        else {
            try {
                T instance = clazz.newInstance();
                ((BaseMyBatisDomain) instance).setMyBatis(this);
                domainMap.put(clazz, instance);
                return instance;
            }
            catch (JingException e) {
                throw e;
            }
            catch (Throwable t) {
                throw new JingJdbcException(t, "failed to create domain");
            }
        }
    }
}
